﻿using System;
using System.Configuration;
using System.IO;
using System.Text;
using MLSharp.Classification;
using System.Linq;
using MLSharp.Utilities;

namespace MLSharp.RProject
{
	/// <summary>
	/// A factory that returns R-based neural networks.
	/// </summary>
	public class RNeuralNetworkFactory : IClassifierFactory
	{
		#region Logger

		/// <summary>
		/// The logger for the class.
		/// </summary>
		private static readonly log4net.ILog mLogger =
			log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

		#endregion

		#region Private Fields

		/// <summary>
		/// The path to R.exe.
		/// </summary>
		private readonly string mRPath;

		/// <summary>
		/// The path to the working directory.
		/// </summary>
		private readonly string mWorkingDirectory;

		#endregion

		#region Public Properties

		/// <summary>
		/// The options to use when building neural networks.
		/// </summary>
		public RNeuralNetOptions Options { get; private set; }

		/// <summary>
		/// Wrapper for Options.
		/// </summary>
		object IClassifierFactory.Options
		{
			get { return Options; }
		}

		#endregion

		#region Public Constructors

		/// <summary>
		/// Creates a factory.
		/// </summary>
		/// <remarks>
		/// You can configure R using the app.config file.  You can specify:
		/// <list>
		///		<item>RPath - the full path to R.exe.</item>
		///		<item>RWorkingDirectory - A directory to place intermediate output into.</item>
		/// </list>
		/// </remarks>
		public RNeuralNetworkFactory()
		{
			mRPath = ConfigurationManager.AppSettings["RPath"] ?? Path.GetFullPath("R.exe");
			mWorkingDirectory = ConfigurationManager.AppSettings["RWorkingDirectory"] ?? Path.GetFullPath(@".\R");

			Initialize();
		}

		/// <summary>
		/// Creates a factory using the specified paths.
		/// </summary>
		/// <param name="rPath">The path to R.exe.</param>
		/// <param name="workingDirectory">The path to the working directory.</param>
		public RNeuralNetworkFactory(string rPath, string workingDirectory)
		{
			mRPath = Path.GetFullPath(rPath);
			mWorkingDirectory = Path.GetFullPath(workingDirectory);

			Initialize();
		}

		#endregion

		#region Private Methods

		/// <summary>
		/// Performs common initialization.
		/// </summary>
		private void Initialize()
		{
			Options = new RNeuralNetOptions();

			if (!Directory.Exists(mWorkingDirectory))
			{
				Directory.CreateDirectory(mWorkingDirectory);
			}

			ValidateConfiguration();
		}

		/// <summary>
		/// Verifies that fields have been set to correct, valid values.
		/// </summary>
		private void ValidateConfiguration()
		{
			//Make sure the R executable path is valid.
			if (!File.Exists(mRPath))
			{
				throw new FileNotFoundException(string.Format("Unable to locate R.exe at '{0}'", mRPath));
			}
		}

		/// <summary>
		/// Creates a string containing the number of neurons per layer formatted
		/// for consumption by R.
		/// </summary>
		/// <returns>A correctly formatted input string.</returns>
		private string GetNeuronCounts(int variableCount)
		{
			//If the user didn't specify any neuron counts, default to a single layer with 2*the number of input
			//variables.
			if (Options.NeuronCounts == null || Options.NeuronCounts.Length == 0)
			{
				return (2*variableCount).ToString();
			}
			//Otherwise, convert the neuron counts to a string representation.
			else
			{
				return string.Join(",", Options.NeuronCounts.Select(c=> c.ToString()).ToArray());
			}
		}

		/// <summary>
		/// Gets a representation of the activation function.
		/// </summary>
		/// <returns></returns>
		private string GetActivationFunction()
		{
			//If the user hasn't specified neuron counts, assume one layer (plus the input layer)
			if (Options.NeuronCounts == null || Options.NeuronCounts.Length == 0)
			{
				return string.Format("c({0},{0})", (int)Options.ActivationFunction);
			}
			//If the user has specified neuron counts, the activation function vector will be that long + 1
			else
			{
				StringBuilder sb = new StringBuilder();

				sb.Append("c(");

				for (int i = 0; i <= Options.NeuronCounts.Length; i++)
				{
					if (i > 0)
					{
						sb.Append(",");
					}

					sb.Append((int)Options.ActivationFunction);
				}

				sb.Append(")");

				return sb.ToString();
			}
		}

		#endregion

		#region Public Methods

		/// <summary>
		/// Builds a neural network that can predict the specified target attribute.
		/// </summary>
		/// <param name="dataSet">The data set to train the network on.</param>
		/// <returns>The neural network classifier.</returns>
		public INeuralNetClassifier BuildNeuralNetwork(IDataSet dataSet)
		{
			//This is the actual R script that will be generated.
			string rScriptName = Path.Combine(mWorkingDirectory, NameGenerator.GetRandomName(10) + ".R");
			//And these are the input variables for the instances
			string rInputTableName = Path.Combine(mWorkingDirectory, NameGenerator.GetRandomName(10) + ".InpTable");
			//And these are the instance target/output variables
			string rTargetTableName = Path.Combine(mWorkingDirectory, NameGenerator.GetRandomName(10) + ".TarTable");

			//The number of independent attributes that are in the data set.
			int independentAttributeCount = dataSet.Attributes.Count - 1;

			// Write the training data to a .table file
			RTableWriter.Write(dataSet, rInputTableName, rTargetTableName);

			using (StreamWriter writer = File.CreateText(rScriptName))
			{
				//Step 1: Write the header part of an R script.
				writer.WriteLine("library(neural)");

				//Step 2 was moved outside the using() block to minimize how long the file handle was open for.

				//Step 3: Write the command to load the inputs table
				writer.WriteLine("x<-as.matrix(read.table(\"{0}\", header = FALSE))", rInputTableName.Replace(@"\", @"\\"));

				//Step 4: Write the command to load the outputs table
				writer.WriteLine("y<-as.matrix(read.table(\"{0}\", header = FALSE))", rTargetTableName.Replace(@"\", @"\\"));

				//This command will train a new network.
				string trainNetwork =
					string.Format(
						"net<-mlptrain(x,c({0}),y,it={1},alfa={2},online={3},permute={4},thresh={5},visual=FALSE,actfns={6})",
						GetNeuronCounts(independentAttributeCount), Options.MaxIterations, Options.LearningRate,
						Options.Online ? "TRUE" : "FALSE", Options.Permutate ? "TRUE" : "FALSE", Options.MaxError,
						GetActivationFunction());

				if (mLogger.IsInfoEnabled)
				{
					mLogger.InfoFormat("Training network: {0}", trainNetwork);
				}

				//Step 5: Write the training command to the R script
				writer.WriteLine(trainNetwork);

				//Step 6: Write the commands to dump the output
				writer.WriteLine("dump(\"net\",file=stdout())");
			}

			//Step 7: Execute R on the script, redirecting the output
			ProcessOutput output = ProcessWrapper.Run(mRPath, mWorkingDirectory, string.Format("-f \"{0}\" --slave", rScriptName));

			//Step 8: Parse the output and extract the network parameters.
			//The output had better begin with a network assignment, or something has gone wrong
			if (!output.StandardOutput.StartsWith("net <-"))
			{
				throw new InvalidOperationException("Unexpected output from R.exe, begins with '" +
				                                    output.StandardOutput.Substring(0, Math.Min(20, output.StandardOutput.Length)) +
				                                    "'");
			}

			//The output should end with '))\r\n'
			if (!output.StandardOutput.EndsWith("))\r\n"))
			{
				throw new InvalidOperationException("Unexpected output fro R.exe, ends with '" +
				                                    output.StandardOutput.Substring(Math.Max(0, output.StandardOutput.Length - 20)) +
				                                    "'");
			}

			//Step 9: Cleanup the temp files.
			File.Delete(rScriptName);
			File.Delete(rInputTableName);
			File.Delete(rTargetTableName);

			//Step 10: Return a new network.
			return new RNeuralNetwork(mWorkingDirectory, mRPath, output.StandardOutput);
		}

		#endregion

		#region IClassifierFactory Members

		/// <summary>
		/// Calls <see cref="BuildNeuralNetwork"/>.
		/// </summary>
		/// <param name="trainingData"></param>
		/// <returns></returns>
		IClassifier IClassifierFactory.BuildClassifier(IDataSet trainingData)
		{
			return BuildNeuralNetwork(trainingData);
		}

		#endregion
	}
}
